/*
* MIT Licensed
* http://www.23developer.com/opensource
* http://github.com/23/resumable.js
* Steffen Tiedemann Christensen, steffen@23company.com
*/

(function(){
	"use strict";
	
	  var Resumable = function(opts){
		if ( !(this instanceof Resumable) ) {
		  return new Resumable(opts);
		}
		this.version = 1.0;
		// SUPPORTED BY BROWSER?
		// Check if these features are support by the browser:
		// - File object type
		// - Blob object type
		// - FileList object type
		// - slicing files
		this.support = (
					   (typeof(File)!=='undefined')
					   &&
					   (typeof(Blob)!=='undefined')
					   &&
					   (typeof(FileList)!=='undefined')
					   &&
					   (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
					   );
		if(!this.support) return(false);
	
	
		// PROPERTIES
		var $ = this;
		$.files = [];
		$.defaults = {
		  chunkSize:1*1024*1024,
		  forceChunkSize:false,
		  simultaneousUploads:3,
		  fileParameterName:'file',
		  chunkNumberParameterName: 'resumableChunkNumber',
		  chunkSizeParameterName: 'resumableChunkSize',
		  currentChunkSizeParameterName: 'resumableCurrentChunkSize',
		  totalSizeParameterName: 'resumableTotalSize',
		  typeParameterName: 'resumableType',
		  identifierParameterName: 'resumableIdentifier',
		  fileNameParameterName: 'resumableFilename',
		  relativePathParameterName: 'resumableRelativePath',
		  totalChunksParameterName: 'resumableTotalChunks',
		  dragOverClass: 'dragover',
		  throttleProgressCallbacks: 0.5,
		  query:{},
		  headers:{},
		  preprocess:null,
		  preprocessFile:null,
		  method:'multipart',
		  uploadMethod: 'POST',
		  testMethod: 'GET',
		  prioritizeFirstAndLastChunk:false,
		  target:'/',
		  testTarget: null,
		  parameterNamespace:'',
		  testChunks:true,
		  generateUniqueIdentifier:null,
		  getTarget:null,
		  maxChunkRetries:100,
		  chunkRetryInterval:undefined,
		  permanentErrors:[400, 401, 403, 404, 409, 415, 500, 501],
		  maxFiles:undefined,
		  withCredentials:false,
		  xhrTimeout:0,
		  clearInput:true,
		  chunkFormat:'blob',
		  setChunkTypeFromFile:false,
		  maxFilesErrorCallback:function (files, errorCount) {
			var maxFiles = $.getOpt('maxFiles');
			alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
		  },
		  minFileSize:1,
		  minFileSizeErrorCallback:function(file, errorCount) {
			alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
		  },
		  maxFileSize:undefined,
		  maxFileSizeErrorCallback:function(file, errorCount) {
			alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
		  },
		  fileType: [],
		  fileTypeErrorCallback: function(file, errorCount) {
			alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
		  }
		};
		$.opts = opts||{};
		$.getOpt = function(o) {
		  var $opt = this;
		  // Get multiple option if passed an array
		  if(o instanceof Array) {
			var options = {};
			$h.each(o, function(option){
			  options[option] = $opt.getOpt(option);
			});
			return options;
		  }
		  // Otherwise, just return a simple option
		  if ($opt instanceof ResumableChunk) {
			if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
			else { $opt = $opt.fileObj; }
		  }
		  if ($opt instanceof ResumableFile) {
			if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
			else { $opt = $opt.resumableObj; }
		  }
		  if ($opt instanceof Resumable) {
			if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
			else { return $opt.defaults[o]; }
		  }
		};
		$.indexOf = function(array, obj) {
			if (array.indexOf) { return array.indexOf(obj); }     
			for (var i = 0; i < array.length; i++) {
				if (array[i] === obj) { return i; }
			}
			return -1;
		}
	
		// EVENTS
		// catchAll(event, ...)
		// fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file),
		// fileError(file, message), complete(), progress(), error(message, file), pause()
		$.events = [];
		$.on = function(event,callback){
		  $.events.push(event.toLowerCase(), callback);
		};
		$.fire = function(){
		  // `arguments` is an object, not array, in FF, so:
		  var args = [];
		  for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
		  // Find event listeners, and support pseudo-event `catchAll`
		  var event = args[0].toLowerCase();
		  for (var i=0; i<=$.events.length; i+=2) {
			if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
			if($.events[i]=='catchall') $.events[i+1].apply(null,args);
		  }
		  if(event=='fileerror') $.fire('error', args[2], args[1]);
		  if(event=='fileprogress') $.fire('progress');
		};
	
	
		// INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
		var $h = {
		  stopEvent: function(e){
			e.stopPropagation();
			e.preventDefault();
		  },
		  each: function(o,callback){
			if(typeof(o.length)!=='undefined') {
			  for (var i=0; i<o.length; i++) {
				// Array or FileList
				if(callback(o[i])===false) return;
			  }
			} else {
			  for (i in o) {
				// Object
				if(callback(i,o[i])===false) return;
			  }
			}
		  },
		  generateUniqueIdentifier:function(file, event){
			var custom = $.getOpt('generateUniqueIdentifier');
			if(typeof custom === 'function') {
			  return custom(file, event);
			}
			var relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
			var size = file.size;
			return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
		  },
		  contains:function(array,test) {
			var result = false;
	
			$h.each(array, function(value) {
			  if (value == test) {
				result = true;
				return false;
			  }
			  return true;
			});
	
			return result;
		  },
		  formatSize:function(size){
			if(size<1024) {
			  return size + ' bytes';
			} else if(size<1024*1024) {
			  return (size/1024.0).toFixed(0) + ' KB';
			} else if(size<1024*1024*1024) {
			  return (size/1024.0/1024.0).toFixed(1) + ' MB';
			} else {
			  return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
			}
		  },
		  getTarget:function(request, params){
			var target = $.getOpt('target');
	
			if (request === 'test' && $.getOpt('testTarget')) {
			  target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget');
			}
	
			if (typeof target === 'function') {
			  return target(params);
			}
	
			var separator = target.indexOf('?') < 0 ? '?' : '&';
			var joinedParams = params.join('&');
	
			if (joinedParams) target = target + separator + joinedParams;
	
			return target;
		  }
		};
	
		var onDrop = function(e){
		  e.currentTarget.classList.remove($.getOpt('dragOverClass'));
		  $h.stopEvent(e);
	
		  //handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
		  if (e.dataTransfer && e.dataTransfer.items) {
			loadFiles(e.dataTransfer.items, e);
		  }
		  //else handle them as files
		  else if (e.dataTransfer && e.dataTransfer.files) {
			loadFiles(e.dataTransfer.files, e);
		  }
		};
		var onDragLeave = function(e){
		  e.currentTarget.classList.remove($.getOpt('dragOverClass'));
		};
		var onDragOverEnter = function(e) {
		  e.preventDefault();
		  var dt = e.dataTransfer;
		  if ($.indexOf(dt.types, "Files") >= 0) { // only for file drop
			e.stopPropagation();
			dt.dropEffect = "copy";
			dt.effectAllowed = "copy";
			e.currentTarget.classList.add($.getOpt('dragOverClass'));
		  } else { // not work on IE/Edge....
			dt.dropEffect = "none";
			dt.effectAllowed = "none";
		  }
		};
	
		/**
		 * processes a single upload item (file or directory)
		 * @param {Object} item item to upload, may be file or directory entry
		 * @param {string} path current file path
		 * @param {File[]} items list of files to append new items to
		 * @param {Function} cb callback invoked when item is processed
		 */
		function processItem(item, path, items, cb) {
		  var entry;
		  if(item.isFile){
			// file provided
			return item.file(function(file){
			  file.relativePath = path + file.name;
			  items.push(file);
			  cb();
			});
		  }else if(item.isDirectory){
			// item is already a directory entry, just assign
			entry = item;
		  }else if(item instanceof File) {
			items.push(item);
		  }
		  if('function' === typeof item.webkitGetAsEntry){
			// get entry from file object
			entry = item.webkitGetAsEntry();
		  }
		  if(entry && entry.isDirectory){
			// directory provided, process it
			return processDirectory(entry, path + entry.name + '/', items, cb);
		  }
		  if('function' === typeof item.getAsFile){
			// item represents a File object, convert it
			item = item.getAsFile();
			if(item instanceof File) {
			  item.relativePath = path + item.name;
			  items.push(item);
			}
		  }
		  cb(); // indicate processing is done
		}
	
	
		/**
		 * cps-style list iteration.
		 * invokes all functions in list and waits for their callback to be
		 * triggered.
		 * @param  {Function[]}   items list of functions expecting callback parameter
		 * @param  {Function} cb    callback to trigger after the last callback has been invoked
		 */
		function processCallbacks(items, cb){
		  if(!items || items.length === 0){
			// empty or no list, invoke callback
			return cb();
		  }
		  // invoke current function, pass the next part as continuation
		  items[0](function(){
			processCallbacks(items.slice(1), cb);
		  });
		}
	
		/**
		 * recursively traverse directory and collect files to upload
		 * @param  {Object}   directory directory to process
		 * @param  {string}   path      current path
		 * @param  {File[]}   items     target list of items
		 * @param  {Function} cb        callback invoked after traversing directory
		 */
		function processDirectory (directory, path, items, cb) {
		  var dirReader = directory.createReader();
		  var allEntries = [];
	
		  function readEntries () {
			dirReader.readEntries(function(entries){
			  if (entries.length) {
				allEntries = allEntries.concat(entries);
				return readEntries();
			  }
	
			  // process all conversion callbacks, finally invoke own one
			  processCallbacks(
				allEntries.map(function(entry){
				  // bind all properties except for callback
				  return processItem.bind(null, entry, path, items);
				}),
				cb
			  );
			});
		  }
	
		  readEntries();
		}
	
		/**
		 * process items to extract files to be uploaded
		 * @param  {File[]} items items to process
		 * @param  {Event} event event that led to upload
		 */
		function loadFiles(items, event) {
		  if(!items.length){
			return; // nothing to do
		  }
		  $.fire('beforeAdd');
		  var files = [];
		  processCallbacks(
			  Array.prototype.map.call(items, function(item){
				// bind all properties except for callback
				var entry = item;
				if('function' === typeof item.webkitGetAsEntry){
				  entry = item.webkitGetAsEntry();
				}
				return processItem.bind(null, entry, "", files);
			  }),
			  function(){
				if(files.length){
				  // at least one file found
				  appendFilesFromFileList(files, event);
				}
			  }
		  );
		};
	
		var appendFilesFromFileList = function(fileList, event){
		  // check for uploading too many files
		  var errorCount = 0;
		  var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
		  if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
			// if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
			if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
			  $.removeFile($.files[0]);
			} else {
			  o.maxFilesErrorCallback(fileList, errorCount++);
			  return false;
			}
		  }
		  var files = [], filesSkipped = [], remaining = fileList.length;
		  var decreaseReamining = function(){
			if(!--remaining){
			  // all files processed, trigger event
			  if(!files.length && !filesSkipped.length){
				// no succeeded files, just skip
				return;
			  }
			  window.setTimeout(function(){
				$.fire('filesAdded', files, filesSkipped);
			  },0);
			}
		  };
		  $h.each(fileList, function(file){
			var fileName = file.name;
			var fileType = file.type; // e.g video/mp4
			if(o.fileType.length > 0){
			  var fileTypeFound = false;
			  for(var index in o.fileType){
				// For good behaviour we do some inital sanitizing. Remove spaces and lowercase all
				o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase();
	
				// Allowing for both [extension, .extension, mime/type, mime/*]
				var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index];
	
				if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) ||
				  //If MIME type, check for wildcard or if extension matches the files tiletype
				  (extension.indexOf('/') !== -1 && (
					(extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) ||
					fileType === extension
				  ))
				){
				  fileTypeFound = true;
				  break;
				}
			  }
			  if (!fileTypeFound) {
				o.fileTypeErrorCallback(file, errorCount++);
				return true;
			  }
			}
	
			if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
			  o.minFileSizeErrorCallback(file, errorCount++);
			  return true;
			}
			if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
			  o.maxFileSizeErrorCallback(file, errorCount++);
			  return true;
			}
	
			function addFile(uniqueIdentifier){
			  if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){
				file.uniqueIdentifier = uniqueIdentifier;
				var f = new ResumableFile($, file, uniqueIdentifier);
				$.files.push(f);
				files.push(f);
				f.container = (typeof event != 'undefined' ? event.srcElement : null);
				window.setTimeout(function(){
				  $.fire('fileAdded', f, event)
				},0);
			  })()} else {
				filesSkipped.push(file);
			  };
			  decreaseReamining();
			}
			// directories have size == 0
			var uniqueIdentifier = $h.generateUniqueIdentifier(file, event);
			if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){
			  // Promise or Promise-like object provided as unique identifier
			  uniqueIdentifier
			  .then(
				function(uniqueIdentifier){
				  // unique identifier generation succeeded
				  addFile(uniqueIdentifier);
				},
			   function(){
				  // unique identifier generation failed
				  // skip further processing, only decrease file count
				  decreaseReamining();
				}
			  );
			}else{
			  // non-Promise provided as unique identifier, process synchronously
			  addFile(uniqueIdentifier);
			}
		  });
		};
	
		// INTERNAL OBJECT TYPES
		function ResumableFile(resumableObj, file, uniqueIdentifier){
		  var $ = this;
		  $.opts = {};
		  $.getOpt = resumableObj.getOpt;
		  $._prevProgress = 0;
		  $.resumableObj = resumableObj;
		  $.file = file;
		  $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
		  $.size = file.size;
		  $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName;
		  $.uniqueIdentifier = uniqueIdentifier;
		  $._pause = false;
		  $.container = '';
		  $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
		  var _error = uniqueIdentifier !== undefined;
	
		  // Callback when something happens within the chunk
		  var chunkEvent = function(event, message){
			// event can be 'progress', 'success', 'error' or 'retry'
			switch(event){
			case 'progress':
			  $.resumableObj.fire('fileProgress', $, message);
			  break;
			case 'error':
			  $.abort();
			  _error = true;
			  $.chunks = [];
			  $.resumableObj.fire('fileError', $, message);
			  break;
			case 'success':
			  if(_error) return;
			  $.resumableObj.fire('fileProgress', $, message); // it's at least progress
			  if($.isComplete()) {
				$.resumableObj.fire('fileSuccess', $, message);
			  }
			  break;
			case 'retry':
			  $.resumableObj.fire('fileRetry', $);
			  break;
			}
		  };
	
		  // Main code to set up a file object with chunks,
		  // packaged to be able to handle retries if needed.
		  $.chunks = [];
		  $.abort = function(){
			// Stop current uploads
			var abortCount = 0;
			$h.each($.chunks, function(c){
			  if(c.status()=='uploading') {
				c.abort();
				abortCount++;
			  }
			});
			if(abortCount>0) $.resumableObj.fire('fileProgress', $);
		  };
		  $.cancel = function(){
			// Reset this file to be void
			var _chunks = $.chunks;
			$.chunks = [];
			// Stop current uploads
			$h.each(_chunks, function(c){
			  if(c.status()=='uploading')  {
				c.abort();
				$.resumableObj.uploadNextChunk();
			  }
			});
			$.resumableObj.removeFile($);
			$.resumableObj.fire('fileProgress', $);
		  };
		  $.retry = function(){
			$.bootstrap();
			var firedRetry = false;
			$.resumableObj.on('chunkingComplete', function(){
			  if(!firedRetry) $.resumableObj.upload();
			  firedRetry = true;
			});
		  };
		  $.bootstrap = function(){
			$.abort();
			_error = false;
			// Rebuild stack of chunks from file
			$.chunks = [];
			$._prevProgress = 0;
			var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
			var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
			for (var offset=0; offset<maxOffset; offset++) {(function(offset){
				window.setTimeout(function(){
					$.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
					$.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
				},0);
			})(offset)}
			window.setTimeout(function(){
				$.resumableObj.fire('chunkingComplete',$);
			},0);
		  };
		  $.progress = function(){
			if(_error) return(1);
			// Sum up progress across everything
			var ret = 0;
			var error = false;
			$h.each($.chunks, function(c){
			  if(c.status()=='error') error = true;
			  ret += c.progress(true); // get chunk progress relative to entire file
			});
			ret = (error ? 1 : (ret>0.99999 ? 1 : ret));
			ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
			$._prevProgress = ret;
			return(ret);
		  };
		  $.isUploading = function(){
			var uploading = false;
			$h.each($.chunks, function(chunk){
			  if(chunk.status()=='uploading') {
				uploading = true;
				return(false);
			  }
			});
			return(uploading);
		  };
		  $.isComplete = function(){
			var outstanding = false;
			if ($.preprocessState === 1) {
			  return(false);
			}
			$h.each($.chunks, function(chunk){
			  var status = chunk.status();
			  if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
				outstanding = true;
				return(false);
			  }
			});
			return(!outstanding);
		  };
		  $.pause = function(pause){
			  if(typeof(pause)==='undefined'){
				  $._pause = ($._pause ? false : true);
			  }else{
				  $._pause = pause;
			  }
		  };
		  $.isPaused = function() {
			return $._pause;
		  };
		  $.preprocessFinished = function(){
			$.preprocessState = 2;
			$.upload();
		  };
		  $.upload = function () {
			var found = false;
			if ($.isPaused() === false) {
			  var preprocess = $.getOpt('preprocessFile');
			  if(typeof preprocess === 'function') {
				switch($.preprocessState) {
				case 0: $.preprocessState = 1; preprocess($); return(true);
				case 1: return(true);
				case 2: break;
				}
			  }
			  $h.each($.chunks, function (chunk) {
				if (chunk.status() == 'pending' && chunk.preprocessState !== 1) {
				  chunk.send();
				  found = true;
				  return(false);
				}
			  });
			}
			return(found);
		  }
		  $.markChunksCompleted = function (chunkNumber) {
			if (!$.chunks || $.chunks.length <= chunkNumber) {
				return;
			}
			for (var num = 0; num < chunkNumber; num++) {
				$.chunks[num].markComplete = true;
			}
		  };
	
		  // Bootstrap and return
		  $.resumableObj.fire('chunkingStart', $);
		  $.bootstrap();
		  return(this);
		}
	
	
		function ResumableChunk(resumableObj, fileObj, offset, callback){
		  var $ = this;
		  $.opts = {};
		  $.getOpt = resumableObj.getOpt;
		  $.resumableObj = resumableObj;
		  $.fileObj = fileObj;
		  $.fileObjSize = fileObj.size;
		  $.fileObjType = fileObj.file.type;
		  $.offset = offset;
		  $.callback = callback;
		  $.lastProgressCallback = (new Date);
		  $.tested = false;
		  $.retries = 0;
		  $.pendingRetry = false;
		  $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
		  $.markComplete = false;
	
		  // Computed properties
		  var chunkSize = $.getOpt('chunkSize');
		  $.loaded = 0;
		  $.startByte = $.offset*chunkSize;
		  $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
		  if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
			// The last chunk will be bigger than the chunk size, but less than 2*chunkSize
			$.endByte = $.fileObjSize;
		  }
		  $.xhr = null;
	
		  // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
		  $.test = function(){
			// Set up request and listen for event
			$.xhr = new XMLHttpRequest();
	
			var testHandler = function(e){
			  $.tested = true;
			  var status = $.status();
			  if(status=='success') {
				$.callback(status, $.message());
				$.resumableObj.uploadNextChunk();
			  } else {
				$.send();
			  }
			};
			$.xhr.addEventListener('load', testHandler, false);
			$.xhr.addEventListener('error', testHandler, false);
			$.xhr.addEventListener('timeout', testHandler, false);
	
			// Add data from the query options
			var params = [];
			var parameterNamespace = $.getOpt('parameterNamespace');
			var customQuery = $.getOpt('query');
			if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
			$h.each(customQuery, function(k,v){
			  params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('='));
			});
			// Add extra data to identify chunk
			params = params.concat(
			  [
				// define key/value pairs for additional parameters
				['chunkNumberParameterName', $.offset + 1],
				['chunkSizeParameterName', $.getOpt('chunkSize')],
				['currentChunkSizeParameterName', $.endByte - $.startByte],
				['totalSizeParameterName', $.fileObjSize],
				['typeParameterName', $.fileObjType],
				['identifierParameterName', $.fileObj.uniqueIdentifier],
				['fileNameParameterName', $.fileObj.fileName],
				['relativePathParameterName', $.fileObj.relativePath],
				['totalChunksParameterName', $.fileObj.chunks.length]
			  ].filter(function(pair){
				// include items that resolve to truthy values
				// i.e. exclude false, null, undefined and empty strings
				return $.getOpt(pair[0]);
			  })
			  .map(function(pair){
				// map each key/value pair to its final form
				return [
				  parameterNamespace + $.getOpt(pair[0]),
				  encodeURIComponent(pair[1])
				].join('=');
			  })
			);
			// Append the relevant chunk and send it
			$.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params));
			$.xhr.timeout = $.getOpt('xhrTimeout');
			$.xhr.withCredentials = $.getOpt('withCredentials');
			// Add data from header options
			var customHeaders = $.getOpt('headers');
			if(typeof customHeaders === 'function') {
			  customHeaders = customHeaders($.fileObj, $);
			}
			$h.each(customHeaders, function(k,v) {
			  $.xhr.setRequestHeader(k, v);
			});
			$.xhr.send(null);
		  };
	
		  $.preprocessFinished = function(){
			$.preprocessState = 2;
			$.send();
		  };
	
		  // send() uploads the actual data in a POST call
		  $.send = function(){
			var preprocess = $.getOpt('preprocess');
			if(typeof preprocess === 'function') {
			  switch($.preprocessState) {
			  case 0: $.preprocessState = 1; preprocess($); return;
			  case 1: return;
			  case 2: break;
			  }
			}
			if($.getOpt('testChunks') && !$.tested) {
			  $.test();
			  return;
			}
	
			// Set up request and listen for event
			$.xhr = new XMLHttpRequest();
	
			// Progress
			$.xhr.upload.addEventListener('progress', function(e){
			  if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
				$.callback('progress');
				$.lastProgressCallback = (new Date);
			  }
			  $.loaded=e.loaded||0;
			}, false);
			$.loaded = 0;
			$.pendingRetry = false;
			$.callback('progress');
	
			// Done (either done, failed or retry)
			var doneHandler = function(e){
			  var status = $.status();
			  if(status=='success'||status=='error') {
				$.callback(status, $.message());
				$.resumableObj.uploadNextChunk();
			  } else {
				$.callback('retry', $.message());
				$.abort();
				$.retries++;
				var retryInterval = $.getOpt('chunkRetryInterval');
				if(retryInterval !== undefined) {
				  $.pendingRetry = true;
				  setTimeout($.send, retryInterval);
				} else {
				  $.send();
				}
			  }
			};
			$.xhr.addEventListener('load', doneHandler, false);
			$.xhr.addEventListener('error', doneHandler, false);
			$.xhr.addEventListener('timeout', doneHandler, false);
	
			// Set up the basic query data from Resumable
			var query = [
			  ['chunkNumberParameterName', $.offset + 1],
			  ['chunkSizeParameterName', $.getOpt('chunkSize')],
			  ['currentChunkSizeParameterName', $.endByte - $.startByte],
			  ['totalSizeParameterName', $.fileObjSize],
			  ['typeParameterName', $.fileObjType],
			  ['identifierParameterName', $.fileObj.uniqueIdentifier],
			  ['fileNameParameterName', $.fileObj.fileName],
			  ['relativePathParameterName', $.fileObj.relativePath],
			  ['totalChunksParameterName', $.fileObj.chunks.length],
			].filter(function(pair){
			  // include items that resolve to truthy values
			  // i.e. exclude false, null, undefined and empty strings
			  return $.getOpt(pair[0]);
			})
			.reduce(function(query, pair){
			  // assign query key/value
			  query[$.getOpt(pair[0])] = pair[1];
			  return query;
			}, {});
			// Mix in custom data
			var customQuery = $.getOpt('query');
			if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
			$h.each(customQuery, function(k,v){
			  query[k] = v;
			});
	
			var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice')));
			var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : "");
			var data = null;
			var params = [];
	
			var parameterNamespace = $.getOpt('parameterNamespace');
					if ($.getOpt('method') === 'octet') {
						// Add data from the query options
						data = bytes;
						$h.each(query, function (k, v) {
							params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
						});
					} else {
						// Add data from the query options
						data = new FormData();
						$h.each(query, function (k, v) {
							data.append(parameterNamespace + k, v);
							params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
						});
						if ($.getOpt('chunkFormat') == 'blob') {
							data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName);
						}
						else if ($.getOpt('chunkFormat') == 'base64') {
							var fr = new FileReader();
							fr.onload = function (e) {
								data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result);
								$.xhr.send(data);
							}
							fr.readAsDataURL(bytes);
						}
					}
	
			var target = $h.getTarget('upload', params);
			var method = $.getOpt('uploadMethod');
	
			$.xhr.open(method, target);
			if ($.getOpt('method') === 'octet') {
			  $.xhr.setRequestHeader('Content-Type', 'application/octet-stream');
			}
			$.xhr.timeout = $.getOpt('xhrTimeout');
			$.xhr.withCredentials = $.getOpt('withCredentials');
			// Add data from header options
			var customHeaders = $.getOpt('headers');
			if(typeof customHeaders === 'function') {
			  customHeaders = customHeaders($.fileObj, $);
			}
	
			$h.each(customHeaders, function(k,v) {
			  $.xhr.setRequestHeader(k, v);
			});
	
			if ($.getOpt('chunkFormat') == 'blob') {
				$.xhr.send(data);
			}
		  };
		  $.abort = function(){
			// Abort and reset
			if($.xhr) $.xhr.abort();
			$.xhr = null;
		  };
		  $.status = function(){
			// Returns: 'pending', 'uploading', 'success', 'error'
			if($.pendingRetry) {
			  // if pending retry then that's effectively the same as actively uploading,
			  // there might just be a slight delay before the retry starts
			  return('uploading');
			} else if($.markComplete) {
			  return 'success';
			} else if(!$.xhr) {
			  return('pending');
			} else if($.xhr.readyState<4) {
			  // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
			  return('uploading');
			} else {
			  if($.xhr.status == 200 || $.xhr.status == 201) {
				// HTTP 200, 201 (created)
				return('success');
			  } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
				// HTTP 400, 404, 409, 415, 500, 501 (permanent error)
				return('error');
			  } else {
				// this should never happen, but we'll reset and queue a retry
				// a likely case for this would be 503 service unavailable
				$.abort();
				return('pending');
			  }
			}
		  };
		  $.message = function(){
			return($.xhr ? $.xhr.responseText : '');
		  };
		  $.progress = function(relative){
			if(typeof(relative)==='undefined') relative = false;
			var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
			if($.pendingRetry) return(0);
			if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95;
			var s = $.status();
			switch(s){
			case 'success':
			case 'error':
			  return(1*factor);
			case 'pending':
			  return(0*factor);
			default:
			  return($.loaded/($.endByte-$.startByte)*factor);
			}
		  };
		  return(this);
		}
	
		// QUEUE
		$.uploadNextChunk = function(){
		  var found = false;
	
		  // In some cases (such as videos) it's really handy to upload the first
		  // and last chunk of a file quickly; this let's the server check the file's
		  // metadata and determine if there's even a point in continuing.
		  if ($.getOpt('prioritizeFirstAndLastChunk')) {
			$h.each($.files, function(file){
			  if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
				file.chunks[0].send();
				found = true;
				return(false);
			  }
			  if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
				file.chunks[file.chunks.length-1].send();
				found = true;
				return(false);
			  }
			});
			if(found) return(true);
		  }
	
		  // Now, simply look for the next, best thing to upload
		  $h.each($.files, function(file){
			found = file.upload();
			if(found) return(false);
		  });
		  if(found) return(true);
	
		  // The are no more outstanding chunks to upload, check is everything is done
		  var outstanding = false;
		  $h.each($.files, function(file){
			if(!file.isComplete()) {
			  outstanding = true;
			  return(false);
			}
		  });
		  if(!outstanding) {
			// All chunks have been uploaded, complete
			$.fire('complete');
		  }
		  return(false);
		};
	
	
		// PUBLIC METHODS FOR RESUMABLE.JS
		$.assignBrowse = function(domNodes, isDirectory){
		  if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
		  $h.each(domNodes, function(domNode) {
			var input;
			if(domNode.tagName==='INPUT' && domNode.type==='file'){
			  input = domNode;
			} else {
			  input = document.createElement('input');
			  input.setAttribute('type', 'file');
			  input.style.display = 'none';
			  domNode.addEventListener('click', function(){
				input.style.opacity = 0;
				input.style.display='block';
				input.focus();
				input.click();
				input.style.display='none';
			  }, false);
			  domNode.appendChild(input);
			}
			var maxFiles = $.getOpt('maxFiles');
			if (typeof(maxFiles)==='undefined'||maxFiles!=1){
			  input.setAttribute('multiple', 'multiple');
			} else {
			  input.removeAttribute('multiple');
			}
			if(isDirectory){
			  input.setAttribute('webkitdirectory', 'webkitdirectory');
			} else {
			  input.removeAttribute('webkitdirectory');
			}
			var fileTypes = $.getOpt('fileType');
			if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) {
			  input.setAttribute('accept', fileTypes.map(function (e) {
				e = e.replace(/\s/g, '').toLowerCase();
				if(e.match(/^[^.][^/]+$/)){
				  e = '.' + e;
				}
				return e;
			  }).join(','));
			}
			else {
			  input.removeAttribute('accept');
			}
			// When new files are added, simply append them to the overall list
			input.addEventListener('change', function(e){
			  appendFilesFromFileList(e.target.files,e);
			  var clearInput = $.getOpt('clearInput');
			  if (clearInput) {
				e.target.value = '';
			  }
			}, false);
		  });
		};
		$.assignDrop = function(domNodes){
		  if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
	
		  $h.each(domNodes, function(domNode) {
			domNode.addEventListener('dragover', onDragOverEnter, false);
			domNode.addEventListener('dragenter', onDragOverEnter, false);
			domNode.addEventListener('dragleave', onDragLeave, false);
			domNode.addEventListener('drop', onDrop, false);
		  });
		};
		$.unAssignDrop = function(domNodes) {
		  if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
	
		  $h.each(domNodes, function(domNode) {
			domNode.removeEventListener('dragover', onDragOverEnter);
			domNode.removeEventListener('dragenter', onDragOverEnter);
			domNode.removeEventListener('dragleave', onDragLeave);
			domNode.removeEventListener('drop', onDrop);
		  });
		};
		$.isUploading = function(){
		  var uploading = false;
		  $h.each($.files, function(file){
			if (file.isUploading()) {
			  uploading = true;
			  return(false);
			}
		  });
		  return(uploading);
		};
		$.upload = function(){
		  // Make sure we don't start too many uploads at once
		  if($.isUploading()) return;
		  // Kick off the queue
		  $.fire('uploadStart');
		  for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
			$.uploadNextChunk();
		  }
		};
		$.pause = function(){
		  // Resume all chunks currently being uploaded
		  $h.each($.files, function(file){
			file.abort();
		  });
		  $.fire('pause');
		};
		$.cancel = function(){
		  $.fire('beforeCancel');
		  for(var i = $.files.length - 1; i >= 0; i--) {
			$.files[i].cancel();
		  }
		  $.fire('cancel');
		};
		$.progress = function(){
		  var totalDone = 0;
		  var totalSize = 0;
		  // Resume all chunks currently being uploaded
		  $h.each($.files, function(file){
			totalDone += file.progress()*file.size;
			totalSize += file.size;
		  });
		  return(totalSize>0 ? totalDone/totalSize : 0);
		};
		$.addFile = function(file, event){
		  appendFilesFromFileList([file], event);
		};
		$.addFiles = function(files, event){
		  appendFilesFromFileList(files, event);
		};
		$.removeFile = function(file){
		  for(var i = $.files.length - 1; i >= 0; i--) {
			if($.files[i] === file) {
			  $.files.splice(i, 1);
			}
		  }
		};
		$.getFromUniqueIdentifier = function(uniqueIdentifier){
		  var ret = false;
		  $h.each($.files, function(f){
			if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
		  });
		  return(ret);
		};
		$.getSize = function(){
		  var totalSize = 0;
		  $h.each($.files, function(file){
			totalSize += file.size;
		  });
		  return(totalSize);
		};
		$.handleDropEvent = function (e) {
		  onDrop(e);
		};
		$.handleChangeEvent = function (e) {
		  appendFilesFromFileList(e.target.files, e);
		  e.target.value = '';
		};
		$.updateQuery = function(query){
			$.opts.query = query;
		};
	
		return(this);
	  };
	
	
	  // Node.js-style export for Node and Component
	  if (typeof module != 'undefined') {
		// left here for backwards compatibility
		module.exports = Resumable;
		module.exports.Resumable = Resumable;
	  } else if (typeof define === "function" && define.amd) {
		// AMD/requirejs: Define the module
		define(function(){
		  return Resumable;
		});
	  } else {
		// Browser: Expose to window
		window.Resumable = Resumable;
	  }
	
	})();